/**
 * Copyright Terracotta, Inc.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.quickstart.ehcache.shiro;

import org.apache.shiro.ShiroException;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.io.ResourceUtils;
import org.apache.shiro.util.Destroyable;
import org.apache.shiro.util.Initializable;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.xml.XmlConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.MalformedURLException;
import java.net.URL;

/**
 * Shiro {@link CacheManager} implementation using the Ehcache 3.x framework for all cache functionality
 */
public class EhcacheShiroManager implements CacheManager, Initializable, Destroyable {

    private static final Logger log = LoggerFactory.getLogger(EhcacheShiroManager.class);

    private volatile org.ehcache.CacheManager manager;

    private volatile String cacheManagerConfigFile = "classpath:/shiro/ehcache.xml";
    private volatile boolean cacheManagerImplicitlyCreated = false;

    private volatile XmlConfiguration cacheConfiguration = null;

    /**
     * Returns the wrapped {@link org.ehcache.CacheManager} instance
     *
     * @return the wrapped {@link org.ehcache.CacheManager} instance
     */
    public org.ehcache.CacheManager getCacheManager() {
        return manager;
    }

    /**
     * Sets the wrapped {@link org.ehcache.CacheManager} instance
     *
     * @param cacheManager the {@link org.ehcache.CacheManager} to be used
     */
    public void setCacheManager(org.ehcache.CacheManager cacheManager) {
        try {
            destroy();
        } catch (Exception e) {
            log.warn("The Shiro managed CacheManager threw an Exception while closing", e);
        }
        manager = cacheManager;
        cacheManagerImplicitlyCreated = false;
    }

    /**
     * Returns the resource location of the config file used to initialize a new EhCache CacheManager instance. The string can be any resource path supported by the
     * {@link org.apache.shiro.io.ResourceUtils#getInputStreamForPath(String)} call.
     * <P>
     * This property is ignored if the CacheManager instance is injected directly - that is, it is only used to lazily create a CacheManager if one is not already provided.
     * </P>
     *
     * @return the resource location of the config file used to initialize the wrapped EhCache CacheManager instance.
     */
    public String getCacheManagerConfigFile() {
        return cacheManagerConfigFile;
    }

    /**
     * Sets the resource location of the config file used to initialize the wrapped EhCache CacheManager instance. The string can be any resource path supported by the
     * {@link org.apache.shiro.io.ResourceUtils#getInputStreamForPath(String)} call.
     * <P>
     * This property is ignored if the CacheManager instance is injected directly - that is, it is only used to lazily create a CacheManager if one is not already provided.
     * </P>
     *
     * @param cacheManagerConfigFile resource location of the config file used to create the wrapped EhCache CacheManager instance.
     */
    public void setCacheManagerConfigFile(String cacheManagerConfigFile) {
        this.cacheManagerConfigFile = cacheManagerConfigFile;
    }

    /**
     * {@inheritDoc}
     */
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        log.trace("Acquiring EhcacheShiro instance named [{}]", name);

        try {
            org.ehcache.Cache<Object, Object> cache = ensureCacheManager().getCache(name, Object.class, Object.class);

            if (cache == null) {
                log.info("Cache with name {} does not yet exist.  Creating now.", name);
                cache = createCache(name);
                log.info("Added EhcacheShiro named [{}]", name);
            } else {
                log.info("Using existing EhcacheShiro named [{}]", name);
            }

            return new EhcacheShiro<K, V>(cache);
        } catch (MalformedURLException e) {
            throw new CacheException(e);
        }
    }

    private org.ehcache.Cache<Object, Object> createCache(String name) {
        try {
            XmlConfiguration xmlConfiguration = getConfiguration();
            CacheConfigurationBuilder<Object, Object> configurationBuilder = xmlConfiguration.newCacheConfigurationBuilderFromTemplate("defaultCacheConfiguration", Object.class, Object.class);
            CacheConfiguration<Object, Object> cacheConfiguration = configurationBuilder.build();
            return ensureCacheManager().createCache(name, cacheConfiguration);
        } catch (InstantiationException e) {
            throw new CacheException(e);
        } catch (IllegalAccessException e) {
            throw new CacheException(e);
        } catch (ClassNotFoundException e) {
            throw new CacheException(e);
        } catch (MalformedURLException e) {
            throw new CacheException(e);
        }
    }

    private org.ehcache.CacheManager ensureCacheManager() throws MalformedURLException {
        if (manager == null) {
            manager = CacheManagerBuilder.newCacheManager(getConfiguration());
            manager.init();

            cacheManagerImplicitlyCreated = true;
        }

        return manager;
    }

    private URL getResource() throws MalformedURLException {
        String cacheManagerConfigFile = getCacheManagerConfigFile();
        if (cacheManagerConfigFile.startsWith(ResourceUtils.CLASSPATH_PREFIX)) {
            return EhcacheShiroManager.class.getClass().getResource(stripPrefix(cacheManagerConfigFile));
        }

        final String url = ResourceUtils.hasResourcePrefix(cacheManagerConfigFile) ? stripPrefix(cacheManagerConfigFile) : cacheManagerConfigFile;

        return new URL(url);
    }

    private static String stripPrefix(String resourcePath) {
        return resourcePath.substring(resourcePath.indexOf(":") + 1);
    }

    private XmlConfiguration getConfiguration() throws MalformedURLException {
        if (cacheConfiguration == null) {
            cacheConfiguration = new XmlConfiguration(getResource());
        }

        return cacheConfiguration;
    }

    public void destroy() throws Exception {
        if (cacheManagerImplicitlyCreated && manager != null) {
            manager.close();
            manager = null;
        }
    }

    /**
     * Initializes this instance.
     * <P>
     * If a {@link #setCacheManager CacheManager} has been explicitly set (e.g. via Dependency Injection or programatically) prior to calling this method, this method does nothing.
     * </P>
     * <P>
     * However, if no {@code CacheManager} has been set a new {@link org.ehcache.Cache} will be initialized. It will use {@code ehcache.xml} configuration file at the root of the classpath.
     * </P>
     *
     * @throws org.apache.shiro.cache.CacheException if there are any CacheExceptions thrown by EhCache.
     */
    public void init() throws ShiroException {
        try {
            ensureCacheManager();
        } catch (MalformedURLException e) {
            throw new ShiroException(e);
        }
    }
}
